# 二进制数据 buffer
30 秒速记
Buffer= 一段固定长度的原始字节,Node.js处理文件、网络、加密数据都靠它- 本质是
Uint8Array的子类,每个元素是0~255的整数 - 内存在
V8堆外分配,所以大文件读进来不太占JS堆,但会占进程的RSS - 全局可用,推荐显式
require('node:buffer')写清依赖 - 和字符串互转一定要说清编码:
utf8、base64、hex、latin1
Buffer 就是 Node.js 里装原始字节的容器,读文件、收网络包、做加解密,拿到的底层数据都是它。 浏览器里的 JS 早期只有字符串,没有办法直接摆弄二进制,Node.js 就自己造了 Buffer;后来 ES6 有了 TypedArray,Buffer 也改成了 Uint8Array 的子类,所以能用下标读写每个字节、能用 for...of 遍历。它的长度在创建时就定死了,不能 push。和字符串互相转换时一定要带上编码,否则默认 utf8,中文、base64 之类场景最容易在这里出乱码。
理解「二进制数据 buffer」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 buffer 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Buffer 可以理解成一段「不可变长度、可修改内容」的字节数组,它解决的是 JS 早期没有二进制类型的问题。几个关键事实:
- 从
Node 4起,Buffer是Uint8Array的子类,buf[0]、buf.length、buf.subarray()这些TypedArray的能力都能用。 - 字节内容在
V8堆外分配。process.memoryUsage()里看arrayBuffers和external,不在heapUsed。 - 字符串和
Buffer之间的转换必须指定编码,默认utf8。
const buf = Buffer.from('hi中', 'utf8');
console.log(buf); // <Buffer 68 69 e4 b8 ad>
console.log(buf.length); // 5,字节数
console.log(buf.toString('hex')); // 6869e4b8ad
console.log(buf.toString('base64')); // aGnkuK0=
console.log(buf instanceof Uint8Array); // true
日常最常见的用法是:读文件、处理 HTTP 请求体、base64 编解码、计算哈希。只要涉及「字节」而不是「字符」,就该想到它。
面试官追问
追问 1Buffer.from('中').length 是多少?
3。utf8 下一个常见汉字占 3 个字节,length 是字节数不是字符数,算上传大小、协议长度时要用 Buffer.byteLength(str)。
追问 2一个 Buffer 能不能当成 Uint8Array 传给只接受 Uint8Array 的库?
能,Buffer 就是它的子类。反过来把 Uint8Array 转成 Buffer 用 Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength),不拷贝内存。
追问 3fs.readFile 读一个 500MB 的文件,heapUsed 没怎么涨,进程内存却很高,为什么?
Buffer 的数据在 V8 堆外,算在 external / arrayBuffers 和 RSS 里,不在 heapUsed。大文件别一次性读,用 fs.createReadStream 分块处理。
追问 4一个 utf8 字符被切在两个 chunk 中间,chunk.toString() 会乱码,怎么办?
用 string_decoder 的 StringDecoder,它会把不完整的字节留到下一块再拼;或者给流设 setEncoding('utf8'),内部就是用它处理的。
# 模块概览
30 秒速记
Buffer是Node.js核心模块,专门处理二进制:文件读写、网络数据、加解密、图片处理- 常用
API按用途分组记:创建、比较、拼接、拷贝、查找、遍历、转换、截取、编码 - 创建:
Buffer.from/alloc/allocUnsafe;拼接:Buffer.concat;比较:equals/compare - 截取用
subarray()(slice()已不推荐),两者都和原Buffer共享内存,改一个另一个也变 - 和字符串转换:
toString(encoding)/Buffer.from(str, encoding)
Buffer 的 API 很多,但按「创建、读写、拼接比较、转换」几组去记就够用了。 创建优先 Buffer.from 和 Buffer.alloc;收网络数据时一块块拿到的 chunk 用 Buffer.concat 拼起来;比较两个 Buffer 内容用 equals,排序用 Buffer.compare;转字符串用 toString 并指定编码。最容易踩坑的是截取:subarray() 和 slice() 都不拷贝内存,返回的是原数据的一个视图,改了截出来的部分原 Buffer 也跟着变,要独立副本就用 Buffer.from(sub) 再拷一份。
Buffer是node的核心模块,开发者可以利用它来处理二进制数据,比如文件流的读写、网络请求数据的处理等。
Buffer的API非常多,本文仅挑选 比较常用/容易理解 的API进行讲解,包括Buffer实例的创建、比较、连接、拷贝、查找、遍历、类型转换、截取、编码转换等。
面试官追问
追问 1const b = buf.slice(0, 2); b[0] = 0;,原来的 buf 会变吗?
会。Buffer 的 slice() 和 subarray() 一样共享内存,不像数组的 slice 是拷贝。要独立的就写 Buffer.from(buf.subarray(0, 2))。
追问 2HTTP 请求体分成多个 chunk 到达,怎么拼成完整数据?
chunk 先 push 进数组,end 时 Buffer.concat(chunks) 一次拼好,再统一 toString('utf8')。不要每个 chunk 直接 += 成字符串,多字节字符会被切坏。
追问 3比较两个 Buffer 内容是否一样,能用 === 吗?
不能,=== 比的是引用。用 a.equals(b);比较密钥、签名这种安全敏感数据要用 crypto.timingSafeEqual(a, b),防时序攻击。
追问 4Buffer 和 base64 字符串怎么互转?
buf.toString('base64') 编码,Buffer.from(str, 'base64') 解码;URL 里用的话换成 'base64url' 编码(Node 15.7+),不会出现 + 和 /。
# 创建
30 秒速记
new Buffer(...)已废弃(DEP0005),Node 10起在非node_modules代码里调用会打运行时警告Buffer.alloc(n):分配n字节并填0,安全Buffer.allocUnsafe(n):不清零,快,但里面可能是旧内存数据,必须马上写满Buffer.from(array | string | buffer | arrayBuffer):按内容创建;传arrayBuffer时共享内存,传buffer时拷贝allocUnsafe小于4KB的走内部8KB内存池,所以更快
创建 Buffer 现在只用三个工厂方法:Buffer.from 按已有内容创建,Buffer.alloc 要一块干净的空内存,Buffer.allocUnsafe 要一块不清零的快速内存。 new Buffer() 被废弃是因为它的行为随参数类型变化太大:传字符串是按内容创建,传数字是按长度分配,而且 Node 8 之前传数字拿到的内存不清零,里面可能有别的请求留下的数据,如果参数来自用户输入就会泄露内存内容。拆成三个方法以后,意图清楚,安全和性能也能自己选。我的默认选择是 from 和 alloc,只有确定马上会把整块写满时才用 allocUnsafe。
- new Buffer(array)
- Buffer.alloc(length)
- Buffer.allocUnsafe(length)
- Buffer.from(array)
创建 Buffer 的几种方式对照着看最清楚:
const a = Buffer.alloc(4); // <Buffer 00 00 00 00>,清零
const b = Buffer.allocUnsafe(4); // 内容不确定,可能是旧数据
const c = Buffer.from([1, 2, 3]); // <Buffer 01 02 03>
const d = Buffer.from('abc'); // <Buffer 61 62 63>,默认 utf8
const e = Buffer.from('YWJj', 'base64'